package np13;

import java.util.LinkedList;
import java.util.Map;

import np13.monads.FunafituMonad;
import np13.utils.TimeTest;

/**
 * {@link Snowden}'s main purpose is to ensure an efficient and fluid
 * redistribution of the {@link Slime}s. This can be achieved by receiving
 * alerts from the {@link FunafituMonad} and handling them appropriately.
 * 
 * {@link Snowden} is also responsible for terminating the program after each
 * slime has been sent to its home land.
 * 
 * Additionally, {@link Snowden} performs the initial distribution of all
 * slimes.
 */
public class Snowden {

	/**
	 * True if {@link Snowden} started working and false otherwise.
	 */
	private boolean started = false;

	/**
	 * An array of (ship size : travel duration) ratios.
	 */
	private float[] ratios;
	/**
	 * A flag if some {@link SlimeQueue} should be filled up.
	 */
	private boolean prio = false;
	/**
	 * A flag which {@link SlimeQueue} should be filled up, represented by its
	 * {@link Smell}.
	 */
	private Smell priority = null;
	/**
	 * A counter, how many {@link FunafituMonad}s are filling the
	 * {@link SlimeQueue}, which should be filled up.
	 */
	private int prioCount = 0;
	/**
	 * A flag that a {@link FunafituMonad} is awaiting an advice.
	 */
	private boolean advice = false;
	/**
	 * A list of {@link Smell}s representing the advice.
	 */
	private LinkedList<Smell> adv = null;
	/**
	 * A number of {@link FunafituMonad}s equals the number of smells.
	 */
	private int monadsDone = Smell.values().length;

	/**
	 * A method to produce an advice, based on actual {@link SlimeQueue} size
	 * and how fast it will be emptied.
	 */
	private void adv() {
		Map<Smell, SlimeQueue> queues = World.getInstance().getFunafitu()
				.getQueues();
		// a list to be save as a result
		LinkedList<Smell> result = new LinkedList<Smell>();
		// a list to save calculated ratio results and sort by
		LinkedList<Float> actualRatios = new LinkedList<Float>();
		int i = 0;
		float mid;
		boolean added = false;
		// run over all smells (queues)
		for (Smell smell : Smell.values()) {
			// calculate the score (needed units to empty the queue)
			mid = queues.get(smell).getCurrentSize() / ratios[i];
			// sort in into result:
			// by first round
			if (!added) {
				actualRatios.add(mid);
				result.add(smell);
				added = true;
				i++;
				continue;
			}
			added = false;
			int j = 0;
			// rest rounds.
			// run over calculated ratios and sort in the new result
			for (Float f : actualRatios) {
				if (mid < f) {
					actualRatios.add(j, mid);
					result.add(j, smell);
					added = true;
					break;
				}
				j++;
			}
			if (!added) {
				actualRatios.add(mid);
				result.add(smell);
			}
			i++;
		}
		// save result and notify the waiting Monads
		adv = result;
		notifyAll();
	}

	/**
	 * {@link FunafituMonad}s seek advice from {@link Snowden}, where to add a
	 * returning Slime.
	 * 
	 * @return A sorted {@link LinkedList} of smells, representing the
	 *         {@link SlimeQueue} with highest priority at the beginning to
	 *         lowest at the end of list.
	 */
	public synchronized LinkedList<Smell> advice() throws InterruptedException {
		// is someone else is being advised, then wait
		while (advice)
			wait();
		// set a flag, that a Monad seeks an advice and notify Snowden
		advice = true;
		notifyAll();
		// wait for an advice
		while (adv == null)
			wait();
		// get the advice, reset the flags and notify Monads waiting for advice
		LinkedList<Smell> res = adv;
		adv = null;
		advice = false;
		notifyAll();
		return res;
	}

	/**
	 * This method is called by {@link FunafituMonad}s once they have finished
	 * filling up some other {@link SlimeQueue}. It decreases the according
	 * counter and notifies {@link Snowden}.
	 */
	public synchronized void doneLoad() {
		prioCount--;
		notifyAll();
	}

	/**
	 * If needed, {@link Snowden} collects all slimes and redistributes them.
	 * This method is also responsible for terminating the program after each
	 * slime has been sent to its home land.
	 * 
	 * @throws InterruptedException
	 */
	private void eventLoop() throws InterruptedException {
		// distribute the slimes from OriginHabitat into Queues
		initial();
		while (started) {
			synchronized (this) {
				// if there is no work to be done, then wait.
				while (!advice && (priority == null || prioCount > 0)
						&& monadsDone > 0) {
					wait();
				}
				// check if an advice is sought
				if (advice)
					adv();
				// check if any Monad is bored
				if (priority != null && prioCount == 0) {
					if (!prio) {
						prio = true;
						letLoad(priority);
					} else {
						World.getInstance().getFunafituMonad().get(priority)
								.mayLoad();
						prio = false;
						priority = null;
					}
				}
				// check if all Monads are done. If so print a result.
				if (monadsDone == 0) {
					for (Smell smell : Smell.values()) {
						System.out.println("Refugee-camp "
								+ smell.name()
								+ " :["
								+ World.getInstance().getHomeLands().get(smell)
										.campSize() + "]");
					}
					started = false;
				}
			}
			// if all work is done, signal the data bank to terminate
			if (!started)
				World.getInstance().getsDDB().terminate();
		}
	}

	/**
	 * Removes all slimes from the origin habitat and places them in an
	 * {@link SlimeQueue}.
	 * 
	 * @throws InterruptedException
	 */
	private void initial() throws InterruptedException {
		LinkedList<Slime> oh = World.getInstance().getFunafitu()
				.getOriginHabitat();
		Map<Smell, SlimeQueue> queues = World.getInstance().getFunafitu()
				.getQueues();
		// overhead is the proximate time for copying a single slime into and
		// out of the boat. This variable makes difference by bigger ferry
		// capacities or by shorter travel times.
		float overhead = 0.0005f;
		float ratioSum = 0f;
		int i = 0;
		Smell ssmell = null;
		// calculating approximate ratio and the highest one
		for (final Smell smell : Smell.values()) {
			ratios[i] = (Configuration.getFerrySize(smell) / (2 * (Configuration
					.getFerryTravelDuration(smell) + (overhead * Configuration
					.getFerrySize(smell)))));
			ratioSum = ratioSum + ratios[i];
			if (i == 0)
				ssmell = smell;
			else if (ratios[i] > ratios[i - 1])
				ssmell = smell;
			i++;
		}
		i = 0;
		int ohSize = oh.size();
		// fill each queue corresponding to its ratio
		System.out.println("--- Snowden staring Slimes to redistribution ---");
		for (final Smell smell : Smell.values()) {
			assert (!oh.isEmpty());
			int r = (int) (ohSize / (ratioSum / ratios[i]));

			for (int j = 0; j < r; j++) {
				queues.get(smell).enqueueSlime(oh.pop());
			}
			i++;

		}
		// put the rest of slimes into a queue with highest ratio
		while (!oh.isEmpty()) {
			queues.get(ssmell).enqueueSlime(oh.pop());
		}
		for (Smell s : Smell.values()) {
			System.out.println(s.name() + " Queue is getting :" + "["
					+ queues.get(s).getCurrentSize() + "]");
		}
	}

	/**
	 * This method is called if some {@link FunafituMonad} is bored and asking
	 * for its {@link SlimeQueue} to be filled up.
	 * 
	 * @param smell
	 *            The smell of {@link SlimeQueue} to be filled up.
	 */
	private void letLoad(Smell smell) {
		Map<Smell, FunafituMonad> monads = World.getInstance()
				.getFunafituMonad();
		LinkedList<Smell> barrier = new LinkedList<Smell>();
		for (Smell s : Smell.values()) {
			if (s == smell)
				continue;
			synchronized (this) {
				if (monads.get(s).loadOther(smell)) {
					barrier.add(s);
					prioCount++;
				}
			}
		}
		for (Smell s : barrier) {
			if (s == smell)
				continue;
			synchronized (this) {
				if (monads.get(s).loadOther(smell))
					;
			}
		}
		synchronized (this) {
			if (prioCount == 0)
				monads.get(smell).lastTrip();
		}
		notifyAll();
	}

	public synchronized void monadDone() {
		monadsDone--;
		System.out.println("Running Monads Thread left: " + monadsDone);
		notifyAll();
	}

	/**
	 * {@link FunafituMonad} inform {@link Snowden} when they are bored.
	 * 
	 * @param smell
	 *            {@link Smell} of the {@link SlimeQueue} which needs to be
	 *            filled up.
	 */
	public synchronized void needMore(Smell smell) {
		// if some other Queue already needs filling then return
		if (priority != null)
			return;
		else {
			// else mark that the queue need filling up and notify Snowden
			priority = smell;
			notifyAll();
		}
	}

	/**
	 * Starts {@link Snowden}. May be called only once.
	 */
	public void start() {
		// System.out.println("Running FunafituMonads: " + monadsDone);
		ratios = new float[World.getInstance().getFunafitu().getQueues().size()];
		if (started)
			throw new IllegalStateException();
		started = true;

		// Thread representing Snowden
		Thread thread = new Thread(new Runnable() {
			@Override
			public void run() {
				try {
					eventLoop();
					TimeTest.stop();
				} catch (InterruptedException e) {
				}
			}
		});
		System.out.println(thread.getName() + " is the one and only Snowden!!");
		thread.setName("Snowden ENDING");
		thread.start();
	}
}
